// jquery.pjax.js
// copyright chris wanstrath
// https://github.com/defunkt/jquery-pjax

(function ($) {

// When called on a container with a selector, fetches the href with
// ajax into the container or with the data-pjax attribute on the link
// itself.
//
// Tries to make sure the back button and ctrl+click work the way
// you'd expect.
//
// Exported as $.fn.pjax
//
// Accepts a jQuery ajax options object that may include these
// pjax specific options:
//
//
// container - Where to stick the response body. Usually a String selector.
//             $(container).html(xhr.responseBody)
//             (default: current jquery context)
//      push - Whether to pushState the URL. Defaults to true (of course).
//   replace - Want to use replaceState instead? That's cool.
//
// For convenience the second parameter can be either the container or
// the options object.
//
// Returns the jQuery object
	function fnPjax(selector, container, options) {
		var context = this
		return this.on('click.pjax', selector, function (event) {
			var opts = $.extend({}, optionsFor(container, options))
			if (!opts.container)
				opts.container = $(this).attr('data-pjax') || context
			handleClick(event, opts)
		})
	}

// Public: pjax on click handler
//
// Exported as $.pjax.click.
//
// event   - "click" jQuery.Event
// options - pjax options
//
// If the click event target has 'data-pjax="0"' attribute, the event is ignored, and no pjax call is made.
//
// Examples
//
//   $(document).on('click', 'a', $.pjax.click)
//   // is the same as
//   $(document).pjax('a')
//
//  $(document).on('click', 'a', function(event) {
//    var container = $(this).closest('[data-pjax-container]')
//    $.pjax.click(event, container)
//  })
//
// Returns nothing.
	function handleClick(event, container, options) {
		options = optionsFor(container, options)

		var link = event.currentTarget

		// Ignore links with data-pjax="0"
		if ($(link).data('pjax') == 0)
			return

		if (link.tagName.toUpperCase() !== 'A')
			throw "$.fn.pjax or $.pjax.click requires an anchor element"

		// Middle click, cmd click, and ctrl click should open
		// links in a new tab as normal.
		if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
			return

		// Ignore cross origin links
		if (location.protocol !== link.protocol || location.hostname !== link.hostname)
			return

		// Ignore anchors on the same page
		if (link.hash && link.href.replace(link.hash, '') ===
			location.href.replace(location.hash, ''))
			return

		// Ignore empty anchor "foo.html#"
		if (link.href === location.href + '#')
			return

		var defaults = {
			url: link.href,
			container: $(link).attr('data-pjax'),
			target: link
		}

		var opts = $.extend({}, defaults, options)
		var clickEvent = $.Event('pjax:click')
		$(link).trigger(clickEvent, [opts])

		if (!clickEvent.isDefaultPrevented()) {
			pjax(opts)
			event.preventDefault()
			$(link).trigger('pjax:clicked', [opts])
		}
	}

// Public: pjax on form submit handler
//
// Exported as $.pjax.submit
//
// event   - "click" jQuery.Event
// options - pjax options
//
// Examples
//
//  $(document).on('submit', 'form', function(event) {
//    var container = $(this).closest('[data-pjax-container]')
//    $.pjax.submit(event, container)
//  })
//
// Returns nothing.
	function handleSubmit(event, container, options) {
		options = optionsFor(container, options)

		var form = event.currentTarget

		if (form.tagName.toUpperCase() !== 'FORM')
			throw "$.pjax.submit requires a form element"

		var defaults = {
			type: form.method.toUpperCase(),
			url: form.action,
			data: $(form).serializeArray(),
			container: $(form).attr('data-pjax'),
			target: form
		}

		pjax($.extend({}, defaults, options))

		event.preventDefault()
	}

// Loads a URL with ajax, puts the response body inside a container,
// then pushState()'s the loaded URL.
//
// Works just like $.ajax in that it accepts a jQuery ajax
// settings object (with keys like url, type, data, etc).
//
// Accepts these extra keys:
//
// container - Where to stick the response body.
//             $(container).html(xhr.responseBody)
//      push - Whether to pushState the URL. Defaults to true (of course).
//   replace - Want to use replaceState instead? That's cool.
//
// Use it just like $.ajax:
//
//   var xhr = $.pjax({ url: this.href, container: '#main' })
//   console.log( xhr.readyState )
//
// Returns whatever $.ajax returns.
	function pjax(options) {
		options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)

		if ($.isFunction(options.url)) {
			options.url = options.url()
		}

		var target = options.target

		var hash = parseURL(options.url).hash

		var context = options.context = findContainerFor(options.container)

		// We want the browser to maintain two separate internal caches: one
		// for pjax'd partial page loads and one for normal page loads.
		// Without adding this secret parameter, some browsers will often
		// confuse the two.
		if (!options.data)
			options.data = {}

		// serializeArray (used in handleSubmit) produces arrays of key/value pairs
		if ($.isArray(options.data))
			options.data.push({ name: '_pjax', value: context.selector })
		else
			options.data._pjax = context.selector

		function fire(type, args) {
			var event = $.Event(type, { relatedTarget: target })
			context.trigger(event, args)
			return !event.isDefaultPrevented()
		}

		var timeoutTimer

		options.beforeSend = function (xhr, settings) {
			// No timeout for non-GET requests
			// Its not safe to request the resource again with a fallback method.
			if (settings.type !== 'GET') {
				settings.timeout = 0
			}

			xhr.setRequestHeader('X-PJAX', 'true')
			xhr.setRequestHeader('X-PJAX-Container', context.selector)

			if (!fire('pjax:beforeSend', [xhr, settings]))
				return false

			if (settings.timeout > 0) {
				timeoutTimer = setTimeout(function () {
					if (fire('pjax:timeout', [xhr, options]))
						xhr.abort('timeout')
				}, settings.timeout)

				// Clear timeout setting so jquerys internal timeout isn't invoked
				settings.timeout = 0
			}

			options.requestUrl = parseURL(settings.url).href
		}

		options.complete = function (xhr, textStatus) {
			if (timeoutTimer)
				clearTimeout(timeoutTimer)

			fire('pjax:complete', [xhr, textStatus, options])

			fire('pjax:end', [xhr, options])
		}

		options.error = function (xhr, textStatus, errorThrown) {
			var container = extractContainer("", xhr, options)
			// Check redirect status code
			var redirect = (xhr.status >= 301 && xhr.status <= 303)
			// Do not fire pjax::error in case of redirect
			var allowed = redirect || fire('pjax:error', [xhr, textStatus, errorThrown, options])
			if (redirect || (options.type == 'GET' && textStatus !== 'abort' && allowed)) {
				locationReplace(container.url)
			}
		}

		options.success = function (data, status, xhr) {
			// If $.pjax.defaults.version is a function, invoke it first.
			// Otherwise it can be a static string.
			var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
				$.pjax.defaults.version() :
				$.pjax.defaults.version

			var latestVersion = xhr.getResponseHeader('X-PJAX-Version')

			var container = extractContainer(data, xhr, options)

			// If there is a layout version mismatch, hard load the new url
			if (currentVersion && latestVersion && currentVersion !== latestVersion) {
				locationReplace(container.url)
				return
			}

			// If the new response is missing a body, hard load the page
			if (!container.contents) {
				locationReplace(container.url)
				return
			}

			pjax.state = {
				id: options.id || uniqueId(),
				url: container.url,
				title: container.title,
				container: context.selector,
				fragment: options.fragment,
				timeout: options.timeout
			}

			if (options.push || options.replace) {
				window.history.replaceState(pjax.state, container.title, container.url)
			}

			// Clear out any focused controls before inserting new page contents.
			document.activeElement.blur()

			if (container.title) document.title = container.title
			context.html(container.contents)

			// FF bug: Won't autofocus fields that are inserted via JS.
			// This behavior is incorrect. So if theres no current focus, autofocus
			// the last field.
			//
			// http://www.w3.org/html/wg/drafts/html/master/forms.html
			var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
			if (autofocusEl && document.activeElement !== autofocusEl) {
				autofocusEl.focus();
			}

			executeScriptTags(container.scripts)

			// Scroll to top by default
			if (typeof options.scrollTo === 'number')
				$(window).scrollTop(options.scrollTo)

			// If the URL has a hash in it, make sure the browser
			// knows to navigate to the hash.
			if (hash !== '') {
				// Avoid using simple hash set here. Will add another history
				// entry. Replace the url with replaceState and scroll to target
				// by hand.
				//
				//   window.location.hash = hash
				var url = parseURL(container.url)
				url.hash = hash

				pjax.state.url = url.href
				window.history.replaceState(pjax.state, container.title, url.href)

				var target = $(url.hash)
				if (target.length) $(window).scrollTop(target.offset().top)
			}

			fire('pjax:success', [data, status, xhr, options])
		}


		// Initialize pjax.state for the initial page load. Assume we're
		// using the container and options of the link we're loading for the
		// back button to the initial page. This ensures good back button
		// behavior.
		if (!pjax.state) {
			pjax.state = {
				id: uniqueId(),
				url: window.location.href,
				title: document.title,
				container: context.selector,
				fragment: options.fragment,
				timeout: options.timeout
			}
			window.history.replaceState(pjax.state, document.title)
		}

		// Cancel the current request if we're already pjaxing
		var xhr = pjax.xhr
		if (xhr && xhr.readyState < 4) {
			xhr.onreadystatechange = $.noop
			xhr.abort()
		}

		// Strip _pjax parameter from URL, if exists.
		options.url = stripPjaxParam(options.url);

		pjax.options = options
		var xhr = pjax.xhr = $.ajax(options)

		if (xhr.readyState > 0) {
			if (options.push && !options.replace) {
				// Cache current container element before replacing it
				cachePush(pjax.state.id, context.clone().contents())

				window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
			}

			fire('pjax:start', [xhr, options])
			fire('pjax:send', [xhr, options])
		}

		return pjax.xhr
	}

// Public: Reload current page with pjax.
//
// Returns whatever $.pjax returns.
	function pjaxReload(container, options) {
		var defaults = {
			url: window.location.href,
			push: false,
			replace: true,
			scrollTo: false
		}

		return pjax($.extend(defaults, optionsFor(container, options)))
	}

// Internal: Hard replace current state with url.
//
// Work for around WebKit
//   https://bugs.webkit.org/show_bug.cgi?id=93506
//
// Returns nothing.
	function locationReplace(url) {
		window.history.replaceState(null, "", "#")
		window.location.replace(url)
	}


	var initialPop = true
	var initialURL = window.location.href
	var initialState = window.history.state

// Initialize $.pjax.state if possible
// Happens when reloading a page and coming forward from a different
// session history.
	if (initialState && initialState.container) {
		pjax.state = initialState
	}

// Non-webkit browsers don't fire an initial popstate event
	if ('state' in window.history) {
		initialPop = false
	}

// popstate handler takes care of the back and forward buttons
//
// You probably shouldn't use pjax on pages with other pushState
// stuff yet.
	function onPjaxPopstate(event) {
		var state = event.state

		if (state && state.container) {
			// When coming forward from a separate history session, will get an
			// initial pop with a state we are already at. Skip reloading the current
			// page.
			if (initialPop && initialURL == state.url) return

			// If popping back to the same state, just skip.
			// Could be clicking back from hashchange rather than a pushState.
			if (pjax.state.id === state.id) return

			var container = $(state.container)
			if (container.length) {
				var direction, contents = cacheMapping[state.id]

				if (pjax.state) {
					// Since state ids always increase, we can deduce the history
					// direction from the previous state.
					direction = pjax.state.id < state.id ? 'forward' : 'back'

					// Cache current container before replacement and inform the
					// cache which direction the history shifted.
					cachePop(direction, pjax.state.id, container.clone().contents())
				}

				var popstateEvent = $.Event('pjax:popstate', {
					state: state,
					direction: direction
				})
				container.trigger(popstateEvent)

				var options = {
					id: state.id,
					url: state.url,
					container: container,
					push: false,
					fragment: state.fragment,
					timeout: state.timeout,
					scrollTo: false
				}

				if (contents) {
					container.trigger('pjax:start', [null, options])

					if (state.title) document.title = state.title
					container.html(contents)
					pjax.state = state

					container.trigger('pjax:end', [null, options])
				} else {
					pjax(options)
				}

				// Force reflow/relayout before the browser tries to restore the
				// scroll position.
				container[0].offsetHeight
			} else {
				locationReplace(location.href)
			}
		}
		initialPop = false
	}

// Fallback version of main pjax function for browsers that don't
// support pushState.
//
// Returns nothing since it retriggers a hard form submission.
	function fallbackPjax(options) {
		var url = $.isFunction(options.url) ? options.url() : options.url,
			method = options.type ? options.type.toUpperCase() : 'GET'

		var form = $('<form>', {
			method: method === 'GET' ? 'GET' : 'POST',
			action: url,
			style: 'display:none'
		})

		if (method !== 'GET' && method !== 'POST') {
			form.append($('<input>', {
				type: 'hidden',
				name: '_method',
				value: method.toLowerCase()
			}))
		}

		var data = options.data
		if (typeof data === 'string') {
			$.each(data.split('&'), function (index, value) {
				var pair = value.split('=')
				form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
			})
		} else if (typeof data === 'object') {
			for (key in data)
				form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
		}

		$(document.body).append(form)
		form.submit()
	}

// Internal: Generate unique id for state object.
//
// Use a timestamp instead of a counter since ids should still be
// unique across page loads.
//
// Returns Number.
	function uniqueId() {
		return (new Date).getTime()
	}

// Internal: Strips _pjax param from url
//
// url - String
//
// Returns String.
	function stripPjaxParam(url) {
		return url
			.replace(/\?_pjax=[^&]+&?/, '?')
			.replace(/_pjax=[^&]+&?/, '')
			.replace(/[\?&]$/, '')
	}

// Internal: Parse URL components and returns a Locationish object.
//
// url - String URL
//
// Returns HTMLAnchorElement that acts like Location.
	function parseURL(url) {
		var a = document.createElement('a')
		a.href = url
		return a
	}

// Internal: Build options Object for arguments.
//
// For convenience the first parameter can be either the container or
// the options object.
//
// Examples
//
//   optionsFor('#container')
//   // => {container: '#container'}
//
//   optionsFor('#container', {push: true})
//   // => {container: '#container', push: true}
//
//   optionsFor({container: '#container', push: true})
//   // => {container: '#container', push: true}
//
// Returns options Object.
	function optionsFor(container, options) {
		// Both container and options
		if (container && options)
			options.container = container

		// First argument is options Object
		else if ($.isPlainObject(container))
			options = container

		// Only container
		else
			options = {container: container}

		// Find and validate container
		if (options.container)
			options.container = findContainerFor(options.container)

		return options
	}

// Internal: Find container element for a variety of inputs.
//
// Because we can't persist elements using the history API, we must be
// able to find a String selector that will consistently find the Element.
//
// container - A selector String, jQuery object, or DOM Element.
//
// Returns a jQuery object whose context is `document` and has a selector.
	function findContainerFor(container) {
		container = $(container)

		if (!container.length) {
			throw "no pjax container for " + container.selector
		} else if (container.selector !== '' && container.context === document) {
			return container
		} else if (container.attr('id')) {
			return $('#' + container.attr('id'))
		} else {
			throw "cant get selector for pjax container!"
		}
	}

// Internal: Filter and find all elements matching the selector.
//
// Where $.fn.find only matches descendants, findAll will test all the
// top level elements in the jQuery object as well.
//
// elems    - jQuery object of Elements
// selector - String selector to match
//
// Returns a jQuery object.
	function findAll(elems, selector) {
		return elems.filter(selector).add(elems.find(selector));
	}

	function parseHTML(html) {
		return $.parseHTML(html, document, true)
	}

// Internal: Extracts container and metadata from response.
//
// 1. Extracts X-PJAX-URL header if set
// 2. Extracts inline <title> tags
// 3. Builds response Element and extracts fragment if set
//
// data    - String response data
// xhr     - XHR response
// options - pjax options Object
//
// Returns an Object with url, title, and contents keys.
	function extractContainer(data, xhr, options) {
		var obj = {}

		// Prefer X-PJAX-URL header if it was set, otherwise fallback to
		// using the original requested url.
		obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)

		// Attempt to parse response html into elements
		if (/<html/i.test(data)) {
			var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
			var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
		} else {
			var $head = $body = $(parseHTML(data))
		}

		// If response data is empty, return fast
		if ($body.length === 0)
			return obj

		// If there's a <title> tag in the header, use it as
		// the page's title.
		obj.title = findAll($head, 'title').last().text()

		if (options.fragment) {
			// If they specified a fragment, look for it in the response
			// and pull it out.
			if (options.fragment === 'body') {
				var $fragment = $body
			} else {
				var $fragment = findAll($body, options.fragment).first()
			}

			if ($fragment.length) {
				obj.contents = $fragment.contents()

				// If there's no title, look for data-title and title attributes
				// on the fragment
				if (!obj.title)
					obj.title = $fragment.attr('title') || $fragment.data('title')
			}

		} else if (!/<html/i.test(data)) {
			obj.contents = $body
		}

		// Clean up any <title> tags
		if (obj.contents) {
			// Remove any parent title elements
			obj.contents = obj.contents.not(function () {
				return $(this).is('title')
			})

			// Then scrub any titles from their descendants
			obj.contents.find('title').remove()

			// Gather all script[src] elements
			obj.scripts = findAll(obj.contents, 'script[src]').remove()
			obj.contents = obj.contents.not(obj.scripts)
		}

		// Trim any whitespace off the title
		if (obj.title) obj.title = $.trim(obj.title)

		return obj
	}

// Load an execute scripts using standard script request.
//
// Avoids jQuery's traditional $.getScript which does a XHR request and
// globalEval.
//
// scripts - jQuery object of script Elements
//
// Returns nothing.
	function executeScriptTags(scripts) {
		if (!scripts) return

		var existingScripts = $('script[src]')

		scripts.each(function () {
			var src = this.src
			var matchedScripts = existingScripts.filter(function () {
				return this.src === src
			})
			if (matchedScripts.length) return

			var script = document.createElement('script')
			script.type = $(this).attr('type') || 'text/javascript';
			script.src = $(this).attr('src')
			document.head.appendChild(script)
		})
	}

// Internal: History DOM caching class.
	var cacheMapping = {}
	var cacheForwardStack = []
	var cacheBackStack = []

// Push previous state id and container contents into the history
// cache. Should be called in conjunction with `pushState` to save the
// previous container contents.
//
// id    - State ID Number
// value - DOM Element to cache
//
// Returns nothing.
	function cachePush(id, value) {
		cacheMapping[id] = value
		cacheBackStack.push(id)

		// Remove all entires in forward history stack after pushing
		// a new page.
		while (cacheForwardStack.length)
			delete cacheMapping[cacheForwardStack.shift()]

		// Trim back history stack to max cache length.
		while (cacheBackStack.length > pjax.defaults.maxCacheLength)
			delete cacheMapping[cacheBackStack.shift()]
	}

// Shifts cache from directional history cache. Should be
// called on `popstate` with the previous state id and container
// contents.
//
// direction - "forward" or "back" String
// id        - State ID Number
// value     - DOM Element to cache
//
// Returns nothing.
	function cachePop(direction, id, value) {
		var pushStack, popStack
		cacheMapping[id] = value

		if (direction === 'forward') {
			pushStack = cacheBackStack
			popStack = cacheForwardStack
		} else {
			pushStack = cacheForwardStack
			popStack = cacheBackStack
		}

		pushStack.push(id)
		if (id = popStack.pop())
			delete cacheMapping[id]
	}

// Public: Find version identifier for the initial page load.
//
// Returns String version or undefined.
	function findVersion() {
		return $('meta').filter(function () {
			var name = $(this).attr('http-equiv')
			return name && name.toUpperCase() === 'X-PJAX-VERSION'
		}).attr('content')
	}

// Install pjax functions on $.pjax to enable pushState behavior.
//
// Does nothing if already enabled.
//
// Examples
//
//     $.pjax.enable()
//
// Returns nothing.
	function enable() {
		$.fn.pjax = fnPjax
		$.pjax = pjax
		$.pjax.enable = $.noop
		$.pjax.disable = disable
		$.pjax.click = handleClick
		$.pjax.submit = handleSubmit
		$.pjax.reload = pjaxReload
		$.pjax.defaults = {
			timeout: 650,
			push: true,
			replace: false,
			type: 'GET',
			dataType: 'html',
			scrollTo: 0,
			maxCacheLength: 20,
			version: findVersion
		}
		$(window).on('popstate.pjax', onPjaxPopstate)
	}

// Disable pushState behavior.
//
// This is the case when a browser doesn't support pushState. It is
// sometimes useful to disable pushState for debugging on a modern
// browser.
//
// Examples
//
//     $.pjax.disable()
//
// Returns nothing.
	function disable() {
		$.fn.pjax = function () {
			return this
		}
		$.pjax = fallbackPjax
		$.pjax.enable = enable
		$.pjax.disable = $.noop
		$.pjax.click = $.noop
		$.pjax.submit = $.noop
		$.pjax.reload = function () {
			window.location.reload()
		}

		$(window).off('popstate.pjax', onPjaxPopstate)
	}


// Add the state property to jQuery's event object so we can use it in
// $(window).bind('popstate')
	if ($.inArray('state', $.event.props) < 0)
		$.event.props.push('state')

// Is pjax supported by this browser?
	$.support.pjax =
		window.history && window.history.pushState && window.history.replaceState &&
			// pushState isn't reliable on iOS until 5.
			!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)

	$.support.pjax ? enable() : disable()

})(jQuery);
